/*
Adapted from AAPlus library by PJ Naughter
Permission was granted to convert the library to C#

This source is modified from PJ Naugters AA+ lib
(http://www.naughter.com/aa.html)
and therefore is covered under his copyright license, which
is as follows:
	
Purpose: Implementation for the algorithms which convert between the Gregorian and Julian calendars and the Julian Day
Created: PJN / 29-12-2003
History: PJN / 10-11-2004 1. Fix for Get so that it works correctly for propalactive calendar dates
         PJN / 15-05-2005 1. Fix for Set(double JD, bool bGregorianCalendarCalendar) not setting the m_bGregorianCalendarCalendar
                          member variable correctly. 
         PJN / 26-01-2006 1. After a bug report from Ing. Taras Kapuszczak that a round trip of the date 25 January 100 as 
                          specified in the Gregorian calendar to the Julian day number and then back again produces the
                          incorrect date 26 January 100, I've spent some time looking into the 2 key Meeus Julian Day
                          algorithms. It seems that the algorithms which converts from a Calendar date to JD works ok for
                          propalactive dates, but the reverse algorithm which converts from a JD to a Calendar date does not.
                          Since I made the change in behaviour to support propalactive Gregorian dates to address issues
                          with the Moslem calendar (and since then I have discovered further unresolved bugs in the Moslem
                          calendar algorithms and advised people to check out my AA+ library instead), I am now reverting
                          these changes so that the date algorithms are now as presented in Meeus's book. This means that 
                          dates after 15 October 1582 are assumed to be in the Gregorian calendar and dates before are 
                          assumed to be in the Julian calendar. This change also means that some of the Date class 
                          methods no longer require the now defunct "bool" parameter to specify which calendar the date 
                          represents. As part of the testing for this release verification code has been added to AATest.cpp
                          to test all the dates from JD 0 (i.e. 1 January -4712) to a date long in the future. Hopefully
                          with this verification code, we should have no more reported issues with the class Date. Again 
                          if you would prefer a much more robust and comprehensive Date time class framework, don't forget 
                          to check out the authors DTime+ library.
                          2. Optimized Date constructor code
                          3. Provided a static version of DaysInMonth() method
                          4. Discovered an issue in JulianToGregorian. It seems the algorithm presented in the
                          book to do conversion from the Julian to Gregorian calendar fails for Julian dates before the 
                          Gregorian calendar reform in 1582. I have sent an email to Jean Meeus to find out if this is a
                          bug in my code or a deficiency in the algorithm presented. Currently the code will assert in this
                          function if it is called for a date before the Gregorian reform.
         PJN / 27-01-2007 1. The static version of the Set method has been renamed to DateToJD to avoid any confusion with
                          the other Set methods. Thanks to Ing. Taras Kapuszczak for reporting this issue.
                          2. The method InGregorianCalendar has now also been renamed to the more appropriate 
                          AfterPapalReform.
                          3. Reinstated the bGregorianCalendar parameter for the Date constructors and Set methods.
                          4. Changed the parameter layout for the static version of DaysInMonth
                          5. Addition of a InGregorianCalendar method.
                          6. Addition of a SetInGregorianCalendar method.
                          7. Reworked implementation of GregorianToJulian method.
                          8. Reworked implementation of JulianToGregorian method.
         PJN / 07-02-2009 1. Updated the static version of DaysInMonth to compile cleanly using code analysis

Copyright (c) 2003 - 2009 by PJ Naughter (Web: www.naughter.com, Email: pjna@naughter.com)

All rights reserved.

Copyright / Usage Details:

You are allowed to include the source code in any product (commercial, shareware, freeware or otherwise) 
when your product is released in binary form. You are allowed to modify the source code in any way you want 
except you cannot modify the copyright details at the top of each module. If you want to distribute source 
code with your application, then you are only allowed to distribute versions released by the author. This is 
to maintain a single distribution point for the source code. 

*/

using System;
using System.Diagnostics;

namespace AAPlus
{
    public class CalendarDate
    {
        public long Year;
        public long Month;
        public long Day;

        public CalendarDate()
        {
        }

        public CalendarDate(long Year, long Month, long Day)
        {
            this.Year = Year;
            this.Month = Month;
            this.Day = Day;
        }

        public CalendarDate(CalendarDate original)
        {
            this.Year = original.Year;
            this.Month = original.Month;
            this.Day = original.Day;
        }
    };

    public class Date
    {

        public enum DAY_OF_WEEK
        {
            SUNDAY = 0,
            MONDAY = 1,
            TUESDAY = 2,
            WEDNESDAY = 3,
            THURSDAY = 4,
            FRIDAY = 5,
            SATURDAY = 6
        };

        static int[] MonthLength = new int[]
  { 
    31, 28, 31, 30, 31, 30,
    31, 31, 30, 31, 30, 31
  };

        /// <summary>
        /// Julian Day number for this date
        /// </summary>
        public double m_dblJulian;

        /// <summary>
        /// Is this date in the Gregorian calendar
        /// </summary>
        public bool m_bGregorianCalendar;

        public Date()
        {
        }

        public Date(long Year, long Month, double Day, bool bGregorianCalendar)
        {
            Set(Year, Month, Day, 0, 0, 0, bGregorianCalendar);
        }

        public Date(long Year, long Month, double Day, double Hour, double Minute, double Second, bool bGregorianCalendar)
        {
            Set(Year, Month, Day, Hour, Minute, Second, bGregorianCalendar);
        }

        public Date(double JD, bool bGregorianCalendar)
        {
            Set(JD, bGregorianCalendar);
        }

        public double Julian()
        {
            return m_dblJulian;
        }

        public bool InGregorianCalendar()
        {
            return m_bGregorianCalendar;
        }

        public static bool AfterPapalReform(long Year, long Month, double Day)
        {
            return ((Year > 1582) || ((Year == 1582) && (Month > 10)) || ((Year == 1582) && (Month == 10) && (Day >= 15)));
        }

        public static bool AfterPapalReform(double JD)
        {
            return (JD >= 2299160.5);
        }

        public static double DateToJD(long Year, long Month, double Day, bool bGregorianCalendar)
        {
            long Y = Year;
            long M = Month;
            if (M < 3)
            {
                Y = Y - 1;
                M = M + 12;
            }

            long A = 0;
            long B = 0;
            if (bGregorianCalendar)
            {
                A = INT(Y / 100.0);
                B = 2 - A + INT(A / 4.0);
            }

            return INT(365.25 * (Y + 4716)) + INT(30.6001 * (M + 1)) + Day + B - 1524.5;
        }

        public static bool IsLeap(long Year, bool bGregorianCalendar)
        {
            if (bGregorianCalendar)
            {
                if ((Year % 100) == 0)
                    return ((Year % 400) == 0) ? true : false;
                else
                    return ((Year % 4) == 0) ? true : false;
            }
            else
                return ((Year % 4) == 0) ? true : false;
        }

        public void Set(long Year, long Month, double Day, double Hour, double Minute, double Second, bool bGregorianCalendar)
        {
            double dblDay = Day + (Hour / 24) + (Minute / 1440) + (Second / 86400);
            Set(DateToJD(Year, Month, dblDay, bGregorianCalendar), bGregorianCalendar);
        }

        public void Get(out long Year, out long Month, out long Day, out long Hour, out long Minute, out double Second)
        {
            double JD = m_dblJulian + 0.5;
            double tempZ;
            double F = MathExtra.Mod(JD, out tempZ);
            long Z = (long)(tempZ);
            long A;

            if (m_bGregorianCalendar) //There is a difference here between the Meeus implementation and this one
            //if (Z >= 2299161)       //The Meeus implementation automatically assumes the Gregorian Calendar 
            //came into effect on 15 October 1582 (JD: 2299161), while the Date
            //implementation has a "m_bGregorianCalendar" value to decide if the date
            //was specified in the Gregorian or Julian Calendars. This difference
            //means in effect that Date fully supports a propalactive version of the
            //Julian calendar. This allows you to construct Julian dates after the Papal
            //reform in 1582. This is useful if you want to construct dates in countries
            //which did not immediately adapt the Gregorian calendar
            {
                long alpha = INT((Z - 1867216.25) / 36524.25);
                A = Z + 1 + alpha - INT(INT(alpha) / 4.0);
            }
            else
                A = Z;

            long B = A + 1524;
            long C = INT((B - 122.1) / 365.25);
            long D = INT(365.25 * C);
            long E = INT((B - D) / 30.6001);

            double dblDay = B - D - INT(30.6001 * E) + F;
            Day = (long)(dblDay);

            if (E < 14)
                Month = E - 1;
            else
                Month = E - 13;

            if (Month > 2)
                Year = C - 4716;
            else
                Year = C - 4715;

            F = MathExtra.Mod(dblDay, out tempZ);
            Hour = INT(F * 24);
            Minute = INT((F - (Hour) / 24.0) * 1440.0);
            Second = (F - (Hour / 24.0) - (Minute / 1440.0)) * 86400.0;
        }

        public void Set(double JD, bool bGregorianCalendar)
        {
            m_dblJulian = JD;
            SetInGregorianCalendar(bGregorianCalendar);
        }

        public void SetInGregorianCalendar(bool bGregorianCalendar)
        {
            bool bAfterPapalReform = (m_dblJulian >= 2299160.5);

#if DEBUG
            if (bGregorianCalendar) //We do not allow storage of propalatic Gregorian dates
                Debug.Assert(bAfterPapalReform);
#endif

            m_bGregorianCalendar = bGregorianCalendar && bAfterPapalReform;
        }

        public long Day()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);
            return Day;
        }

        public long Month()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);
            return Month;
        }

        public long Year()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);
            return Year;
        }

        public long Hour()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);
            return Hour;
        }

        public long Minute()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);
            return Minute;
        }

        public double Second()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);
            return Second;
        }

        public DAY_OF_WEEK DayOfWeek()
        {
            return (DAY_OF_WEEK)((long)(m_dblJulian + 1.5) % 7);
        }

        public static long DaysInMonth(long Month, bool bLeap)
        {
            //Validate our parameters
            Debug.Assert(Month >= 1 && Month <= 12);

            if (bLeap && Month == 2)
                return MonthLength[1]+1;

            return MonthLength[Month - 1];
        }

        public long DaysInMonth()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);

            return DaysInMonth(Month, IsLeap(Year, m_bGregorianCalendar));
        }

        public long DaysInYear()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);

            if (IsLeap(Year, m_bGregorianCalendar))
                return 366;
            else
                return 365;
        }

        public double DayOfYear()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);

            return DayOfYear(m_dblJulian, Year, AfterPapalReform(Year, 1, 1));
        }

        public double DayOfYear(double JD, long Year, bool bGregorianCalendar)
        {
            return JD - DateToJD(Year, 1, 1, bGregorianCalendar) + 1;
        }

        public double FractionalYear()
        {
            long Year = 0;
            long Month = 0;
            long Day = 0;
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            Get(out Year, out Month, out Day, out Hour, out Minute, out Second);

            long DaysInYear;
            if (IsLeap(Year, m_bGregorianCalendar))
                DaysInYear = 366;
            else
                DaysInYear = 365;

            return Year + ((m_dblJulian - DateToJD(Year, 1, 1, AfterPapalReform(Year, 1, 1))) / DaysInYear);
        }

        public bool Leap()
        {
            return IsLeap(Year(), m_bGregorianCalendar);
        }

        public static void DayOfYearToDayAndMonth(long DayOfYear, bool bLeap, out long DayOfMonth, out long Month)
        {
            long K = bLeap ? 1 : 2;

            Month = INT(9 * (K + DayOfYear) / 275.0 + 0.98);
            if (DayOfYear < 32)
                Month = 1;

            DayOfMonth = DayOfYear - INT((275 * Month) / 9.0) + (K * INT((Month + 9) / 12.0)) + 30;
        }

        public static long INT(double value)
        {
            if (value >= 0)
                return (long)(value);
            else
                return (long)(value - 1);
        }

        public static CalendarDate JulianToGregorian(long Year, long Month, long Day)
        {
            Date date = new Date(Year, Month, Day, false);
            date.SetInGregorianCalendar(true);

            CalendarDate GregorianDate = new CalendarDate();
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            date.Get(out GregorianDate.Year, out GregorianDate.Month, out GregorianDate.Day, out Hour, out Minute, out Second);

            return GregorianDate;
        }

        public static CalendarDate GregorianToJulian(long Year, long Month, long Day)
        {
            Date date = new Date(Year, Month, Day, true);
            date.SetInGregorianCalendar(false);

            CalendarDate JulianDate = new CalendarDate();
            long Hour = 0;
            long Minute = 0;
            double Second = 0;
            date.Get(out JulianDate.Year, out JulianDate.Month, out JulianDate.Day, out Hour, out Minute, out Second);

            return JulianDate;
        }

    }
}
